Spring Security | Note-15

Spring Security Note-15


重构注册逻辑

在之前浏览器的社交账号登录注册操作逻辑时,发现用户是第一次用社交帐号登录时,它会跳转至配置的注册页上,跳转到注册页之前,会将第三方用户信息放到SESSION当中;

跳转到注册页之后,可以访问/social/user服务,然后将用户信息从SESSION中提出来,并且提供了providerSignInUtils的根据,从SESSION中拿出用户数据来,一旦用户注册(绑定)完成之后,拿到一个唯一的用户标识,providerSignInUtils再把之前的第三方用户信息拿出来,做一个绑定,存到数据库中;

问题

在APP当中,之前的逻辑是想不通的,因为浏览器是基于SESSION的;

APP是一种属于无SESSION的环境,我们需要对注册逻辑进行改造;

解决

基本的操作思路与验证码的重构一致,我们不将信息存到SESSION当中,而是在传输信息时,先将用户信息存到一个外部存储(REDIS)当中,携带一个deviceId;

存放社交信息工具类AppSignUpUtils
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Component
public class AppSignUpUtils {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
@Autowired
private UsersConnectionRepository usersConnectionRepository;
@Autowired
private ConnectionFactoryLocator connectionFactoryLocator;

public void saveConnectionData(WebRequest request, ConnectionData connectionData) {
redisTemplate.opsForValue().set(getKey(request), connectionData, 10, TimeUnit.MINUTES);
}

public void doPostSignUp(WebRequest request, String userId) {
String key = getKey(request);
if (!redisTemplate.hasKey(key)) {
throw new AppSecretException("无法找到缓存的用户社交账号信息");
}

ConnectionData connectionData = (ConnectionData) redisTemplate.opsForValue().get(key);
Connection<?> connection = connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId()).createConnection(connectionData);

usersConnectionRepository.createConnectionRepository(userId).addConnection(connection);
redisTemplate.delete(key);
}

private String getKey(WebRequest request) {
String deviceId = request.getHeader("deviceId");
if (StringUtils.isBlank(deviceId)) {
throw new AppSecretException("设备ID参数不能为空");
}
return "imooc:security:social.connect." + deviceId;
}
}
SpringSocialConfigurerPostProcessor

SpringSocialConfigurerPostProcessor ,在所有的Bean初始化之前,如果是配置了imoocSocialSecurityConfig,就重行定义注册的处理器;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class SpringSocialConfigurerPostProcessor implements BeanPostProcessor {

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if(StringUtils.equals(beanName,"imoocSocialSecurityConfig")){
ImoocSpringSocialConfigurer configurer = (ImoocSpringSocialConfigurer)bean;
configurer.signupUrl("/social/signUp");
return configurer;
}
return bean;
}
}
处理注册处理器AppSecurityController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class AppSecurityController {
@Autowired
private ProviderSignInUtils providerSignInUtils;
@Autowired
private AppSignUpUtils appSignUpUtils;

@GetMapping("/social/signUp")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public SocialUserInfo getSocialUserInfo(HttpServletRequest request) {
SocialUserInfo userInfo = new SocialUserInfo();
Connection<?> connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
userInfo.setProviderId(connection.getKey().getProviderId());
userInfo.setProviderUserId(connection.getKey().getProviderUserId());
userInfo.setNickname(connection.getDisplayName());
userInfo.setHeadimg(connection.getImageUrl());

appSignUpUtils.saveConnectionData(new ServletWebRequest(request), connection.createData());
return userInfo;
}
}

这样如果在使用社交帐号进行登录时,如果在数据库中没有对应的用户信息,就会引导进行注册,而且不是之前配置的注册页面的逻辑;

此时用户信息已经封装在UserInfo中,并存放在redis中了,在执行注册的请求;


Token处理

基本的Token参数配置

Token的处理都在认证服务器内完成,对ImoocAuthorizationServerConfig进行配置;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@EnableAuthorizationServer
public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).userDetailsService(userDetailsService);
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // Token存储信息的位置
.withClient("imooc") // 指定client-id 配置文件就不会起作用
.secret("imoocsecret") // 指定client-secret 配置文件就不会起作用
.accessTokenValiditySeconds(7200) // 令牌有效期
.authorizedGrantTypes("password", "refresh_token") // 所支持的授权模式
.scopes("all"); // 发出的权限
}
}

返回的结果当中,最明显的就是expires_in;

如果发送时,不带有scope的参数,则会返回所有的scope类型;

-

通用配置类代替
1
2
3
4
5
public class OAuth2ClientProperties {
private String clientId;
private String clientSecret;
private int accessTokenValidateSeconds = 7200;
}
1
2
3
public class OAuth2Properties {
private OAuth2ClientProperties[] clients = {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
@EnableAuthorizationServer
public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SecurityProperties securityProperties;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).userDetailsService(userDetailsService);
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
InMemoryClientDetailsServiceBuilder builder = clients.inMemory();
if(ArrayUtils.isNotEmpty(securityProperties.getOauth2().getClients())){
for(OAuth2ClientProperties config:securityProperties.getOauth2().getClients()){
builder.withClient(config.getClientId()) // 指定client-id 配置文件就不会起作用
.secret(config.getClientSecret()) // 指定client-secret 配置文件就不会起作用
.accessTokenValiditySeconds(config.getAccessTokenValidateSeconds()) // 令牌有效期
.authorizedGrantTypes("password", "refresh_token") // 所支持的授权模式
.scopes("all","read","write"); // 发出的权限
}
}
}
}

-

配置存储方式TokenStoreConfig

现在的存储方式是存储在内存当中的,当服务重启时,就会清除;

此时我们需要将它存储在独立的容器中,例如数据库或Redis;

1
2
3
4
5
6
7
8
9
@Configuration
public class TokenStoreConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore redisTokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
}
1
2
3
4
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore).authenticationManager(authenticationManager).userDetailsService(userDetailsService);
}

-

测试


JWT替换默认令牌

JWT(Json Web Token):是JSON开放的Token标准,与默认的区别在于:

自包含:里面包含有意义的信息。Spring默认的Token是UUID生成的Token,本身无任何意义,本身不包含任何信息,信息是单独保存的。JWT的Token的是包含有意义信息的,如果存放Token的依赖储存器(Redis)奔溃或不可测情况,Token进行解读即可;

密签:为了自包含中的信息不可被随意的修改,并且不包含相关的业务信息,可用指定的密钥进行签名,防止篡改,修改即可知道;

可拓展:所包含的信息可用根据业务需求进行自定义;

-

配置TokenStoreConfig
1
2
3
4
public class OAuth2Properties {
private OAuth2ClientProperties[] clients = {};
private String jwtSigningKey = "imooc";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
public class TokenStoreConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;

@Bean
@ConditionalOnProperty(prefix = "imooc.security.oauth2", name = "storeType", havingValue = "redis")
public TokenStore redisTokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}

@Configuration
@ConditionalOnProperty(prefix = "imooc.security.oauth2", name = "storeType", havingValue = "jwt", matchIfMissing = true)
public static class JwtTokenConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 指定密签
converter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey());
return converter;
}
}
}
修改ImoocAuthorizationServerConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableAuthorizationServer
public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired(required = false)
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore).authenticationManager(authenticationManager).userDetailsService(userDetailsService);
if(jwtAccessTokenConverter != null){
endpoints.accessTokenConverter(jwtAccessTokenConverter);
}
}
}
返回结果(JWT)

自包含解码结果

-

此时再通过access_token去获取用户信息,发现无返回内容;

实际,我们在用户信息/user/me中的参数UserDetails,实际我们传入的参数并不是UserDetails,而是一个字符串,所以无法获取到对应的用户信息;

参数修改为Authentication
1
2
3
4
@GetMapping("/me")
public Object getCurrentUser(Authentication user){
return user;
}

-

自拓展ImoocJwtTokenEnhancer
1
2
3
4
5
6
7
8
9
public class ImoocJwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> info = new HashMap<>();
info.put("company", "imooc");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
添加配置TokenStoreConfig
1
2
3
4
5
@Bean
@ConditionalOnMissingBean(name = "jwtTokenEnhancer")
public TokenEnhancer jwtTokenEnhancer() {
return new ImoocJwtTokenEnhancer();
}
修改认证服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Autowired(required = false)
private TokenEnhancer jwtTokenEnhancer;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore).authenticationManager(authenticationManager).userDetailsService(userDetailsService);
if (jwtAccessTokenConverter != null && jwtTokenEnhancer != null) {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancers = new ArrayList<>();
enhancers.add(jwtTokenEnhancer);
enhancers.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancers);
endpoints.tokenEnhancer(enhancerChain).accessTokenConverter(jwtAccessTokenConverter);
}
}
返回结果

再次访问用户信息,实际上不存在我们自拓展的信息,只包含规范内的信息;

如果需要解析自拓展的信息,则需要以下操作;

解析
添加依赖
1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
@GetMapping("/me")
public Object getCurrentUser(Authentication user, HttpServletRequest request) throws UnsupportedEncodingException {
String header = request.getHeader("Authorization");
String token = StringUtils.substringAfter(header,"bearer ");
Claims claims = Jwts.parser().setSigningKey(securityProperties.getOauth2().getJwtSigningKey().getBytes("UTF-8")).parseClaimsJws(token).getBody();
String company = (String)claims.get("company");
logger.info(company);
return user;
}

令牌刷新

令牌无效后,不能反复登录去重新获取令牌,因为用户会用户体验极差;

在返回refresh_token,在无感知的情况下,获取一个新的access_token;